Oracle sessionless transaction implementation#3887
Conversation
…into sessionless-transaction
…into sessionless-transaction
…into sessionless-transaction
…into sessionless-transaction
There was a problem hiding this comment.
Pull request overview
This PR introduces Oracle JDBC “sessionless transaction” support to Micronaut Data by adding new transaction propagation modes, a dedicated Oracle-aware JDBC transaction manager, and opt-in propagation of suspended transaction identifiers across HTTP and programmatic boundaries. It also adds guide documentation, a runnable doc-example, and unit/integration tests to demonstrate and validate the feature.
Changes:
- Added
SUSPENDandREQUIRES_SUSPENDEDpropagation modes toTransactionDefinitionand wired them into the base transaction operations. - Implemented Oracle-specific JDBC transaction support (manager, propagated state, id codec, optional HTTP server filter, and programmatic propagation API).
- Added documentation plus a new doc-example module and new test coverage (unit + Oracle XE integration).
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/docs/guide/toc.yml | Adds a docs navigation entry for the new Oracle sessionless transactions guide page. |
| src/main/docs/guide/dbc/jdbc/oracleSessionlessTransactions.adoc | New guide page describing sessionless transaction propagation, HTTP filter, and programmatic API. |
| settings.gradle | Includes the new doc-example subproject. |
| doc-examples/jdbc-sessionless-transaction-booking-java/build.gradle | New example build with Oracle test-resources configuration. |
| doc-examples/jdbc-sessionless-transaction-booking-java/src/main/resources/application.yml | Enables HTTP propagation and configures an Oracle datasource for the example. |
| doc-examples/jdbc-sessionless-transaction-booking-java/src/main/resources/logback.xml | Logging configuration for the example app. |
| doc-examples/jdbc-sessionless-transaction-booking-java/src/main/java/example/Application.java | Example application entrypoint. |
| doc-examples/jdbc-sessionless-transaction-booking-java/src/main/java/example/BookingController.java | Example HTTP endpoints demonstrating suspend/resume across requests. |
| doc-examples/jdbc-sessionless-transaction-booking-java/src/main/java/example/BookingService.java | Example service using the new propagation modes. |
| doc-examples/jdbc-sessionless-transaction-booking-java/src/main/java/example/Seat.java | Example entity persisted inside suspended/resumed transactions. |
| doc-examples/jdbc-sessionless-transaction-booking-java/src/main/java/example/SeatRepository.java | Repository used by the example workflow. |
| doc-examples/jdbc-sessionless-transaction-booking-java/src/test/java/example/BookingControllerTest.java | Example test demonstrating HTTP header propagation. |
| doc-examples/jdbc-sessionless-transaction-booking-java/src/test/java/example/BookingServiceTest.java | Example test demonstrating programmatic propagation operations. |
| data-tx/src/main/java/io/micronaut/transaction/TransactionDefinition.java | Adds SUSPEND / REQUIRES_SUSPENDED propagation enum values and Javadoc. |
| data-tx/src/main/java/io/micronaut/transaction/support/AbstractTransactionOperations.java | Treats the new propagation modes as “new transaction” cases in the core orchestration. |
| data-tx-jdbc/src/main/java/io/micronaut/transaction/jdbc/DataSourceTransactionManager.java | Makes the JDBC manager extensible and adds validation/rejection for unsupported sessionless propagation. |
| data-tx-jdbc/src/main/java/io/micronaut/transaction/jdbc/oracle/package-info.java | New Oracle JDBC tx package configuration/requirements. |
| data-tx-jdbc/src/main/java/io/micronaut/transaction/jdbc/oracle/OracleSessionlessTransactionState.java | PropagatedContext element holding the Oracle GTRID. |
| data-tx-jdbc/src/main/java/io/micronaut/transaction/jdbc/oracle/OracleSessionlessTransactionPropagationOperations.java | Public API for non-HTTP propagation scopes. |
| data-tx-jdbc/src/main/java/io/micronaut/transaction/jdbc/oracle/DefaultOracleSessionlessTransactionPropagationOperations.java | Default implementation of programmatic propagation scopes. |
| data-tx-jdbc/src/main/java/io/micronaut/transaction/jdbc/oracle/OracleSessionlessTransactionIdCodec.java | Public API for encoding/decoding transaction identifiers. |
| data-tx-jdbc/src/main/java/io/micronaut/transaction/jdbc/oracle/DefaultOracleSessionlessTransactionIdCodec.java | Default Base64-url codec implementation. |
| data-tx-jdbc/src/main/java/io/micronaut/transaction/jdbc/oracle/OracleSessionlessTransactionHttpConfiguration.java | Configuration properties for HTTP propagation. |
| data-tx-jdbc/src/main/java/io/micronaut/transaction/jdbc/oracle/OracleSessionlessTransactionHttpServerFilter.java | Optional HTTP filter bridging headers ↔ propagated context. |
| data-tx-jdbc/src/main/java/io/micronaut/transaction/jdbc/oracle/OracleSessionlessTransactionManager.java | Oracle JDBC transaction manager implementing suspend/resume semantics. |
| data-tx-jdbc/build.gradle | Adds Micronaut HTTP (compileOnly/test) and Oracle driver test dependency for new HTTP/filter tests. |
| data-tx-jdbc/src/test/groovy/io/micronaut/transaction/jdbc/DataSourceTransactionManagerSpec.groovy | Verifies non-Oracle JDBC manager rejects new propagation modes. |
| data-tx-jdbc/src/test/groovy/io/micronaut/transaction/jdbc/oracle/OracleSessionlessTransactionIdCodecSpec.groovy | Tests default/custom codec bean selection. |
| data-tx-jdbc/src/test/groovy/io/micronaut/transaction/jdbc/oracle/OracleSessionlessTransactionPropagationOperationsSpec.groovy | Tests programmatic propagation scoping behavior. |
| data-tx-jdbc/src/test/groovy/io/micronaut/transaction/jdbc/oracle/OracleSessionlessTransactionHttpServerFilterSpec.groovy | Tests HTTP header read/write and error handling behavior. |
| data-tx-jdbc/src/test/groovy/io/micronaut/transaction/jdbc/oracle/OracleSessionlessTransactionManagerSpec.groovy | Tests Oracle manager begin/commit/rollback behaviors and listener integration. |
| data-jdbc/src/test/groovy/io/micronaut/data/jdbc/oraclexe/sessionless/OracleSessionlessTransactionPropagationSpec.groovy | Oracle XE integration test covering HTTP + programmatic propagation scenarios. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 39 out of 39 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
data-tx/src/main/java/io/micronaut/transaction/support/AbstractTransactionOperations.java:167
- If transaction begin fails after acquiring a new connection from the SynchronousConnectionManager, the connection is never completed because the completion synchronization is registered only after createAndBeginTransaction() returns. Wrap begin in try/catch and ensure synchronousConnectionManager.complete(newConnectionStatus) is called on failure to avoid leaking the connection and leaving it in a non-default state.
TransactionUtil.validateOracleSessionlessPropagation(definition, supportsOracleSessionlessTransactions());
boolean debugEnabled = logger.isDebugEnabled();
if (debugEnabled) {
logger.debug("Getting transaction for definition [{}]", definition);
}
| @NonNull | ||
| @Override | ||
| public T getTransaction(TransactionDefinition definition) throws TransactionException { | ||
| TransactionUtil.validateOracleSessionlessPropagation(definition, supportsOracleSessionlessTransactions()); | ||
| boolean debugEnabled = logger.isDebugEnabled(); | ||
| if (debugEnabled) { | ||
| logger.debug("Getting transaction for definition [{}]", definition); |
There was a problem hiding this comment.
@copilot this seems unrelated to this PR, can you provide another PR resolving this issue
|
@graemerocher I've opened a new pull request, #3890, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
please update the PR description with more detail |
…into sessionless-transaction
…into sessionless-transaction
| * @param definition The transaction definition | ||
| * @param supported Whether the transaction manager supports Oracle sessionless transaction propagation | ||
| */ | ||
| public static void validateOracleSessionlessPropagation(TransactionDefinition definition, boolean supported) { |
There was a problem hiding this comment.
Maybe this can be implemented using a TransactionSynchronization?
There was a problem hiding this comment.
I don’t think TransactionSynchronization is a good fit for this validation because it runs after a transaction status already exists. The purpose of this check is to reject Oracle sessionless modes before an unsupported transaction manager can treat them as a normal transaction.
If we waited until a synchronization could be registered, the unsupported manager may already have selected a connection definition, created a transaction status, or begun a normal transaction.
Even if TransactionSynchronization had an earlier callback, it would still need transaction-manager capability information to know whether the current manager supports Oracle sessionless transactions.
| NESTED | ||
| NESTED, | ||
| /** | ||
| * Start an Oracle sessionless transaction and suspend it instead of committing when the |
There was a problem hiding this comment.
Not sure I like the new Oracle only TX types; Maybe this should be a new annotation like for this logic or OracleTransactional?
There was a problem hiding this comment.
I moved those to OracleTransaction annotation. Now it looks like:
public @interface OracleTransactional {
enum Sessionless {
/**
* Do not apply Oracle sessionless transaction semantics.
*/
NONE,
/**
* Start an Oracle sessionless transaction and suspend it instead of committing when the
* transactional boundary completes.
* <p>The {@link OracleTransactional#timeout()} value is passed to Oracle when the
* sessionless transaction is started.
*/
SUSPEND,
/**
* Resume an Oracle sessionless transaction from the current propagation context and complete
* it when the transactional boundary completes.
*/
REQUIRES_SUSPENDED
}
/**
* The desired Oracle sessionless transaction mode.
*
* @return The sessionless transaction mode
* @since 5.1.0
*/
Sessionless sessionless() default Sessionless.NONE;
| * applications can replace the codec without changing propagation mechanics.</p> | ||
| */ | ||
| @Singleton | ||
| final class DefaultOracleSessionlessTransactionPropagationOperations implements OracleSessionlessTransactionPropagationOperations { |
There was a problem hiding this comment.
Maybe this should be in a special data-tx-jdbc-oracle module
There was a problem hiding this comment.
How this is going to work if we have multiple OracleSessionlessTransactionPropagationOperations?
There was a problem hiding this comment.
micronaut-data-tx-jdbc already contains code related to the Oracle Transaction Priority implementation. If we introduced a new module for Oracle-specific transaction support, we would ideally move all Oracle transaction functionality there, including both transaction priority and sessionless transactions. However, moving the existing priority implementation would be a breaking change. Moving only the sessionless transaction implementation would avoid that break, but it would make the module layout inconsistent because Oracle transaction features would be split across modules.
There was a problem hiding this comment.
The intended usage is to create one propagation scope for the suspend operation, export the transaction id from that scope, and then create a separate propagation scope later when resuming:
SuspendedSeat suspendedSeat = transactionPropagationOperations.withPropagation(() -> {
Long seatId = bookingService.holdSeat(new Seat("JU502", "3a", "msid"));
String transactionId = transactionPropagationOperations.currentTransactionId().orElseThrow();
return new SuspendedSeat(seatId, transactionId);
});
// do something else
transactionPropagationOperations.withPropagation(suspendedSeat.transactionId(), () -> {
bookingService.ticketSeat(suspendedSeat.seatId());
return null;
});
OracleSessionlessTransactionPropagationOperations itself does not own transaction state. It only installs or reads OracleSessionlessTransactionState from the current PropagatedContext. The transaction manager also reads that propagated state directly. OracleSessionlessTransactionState contains only transaction id retrieved from oracle jdbc when a transaction is suspended.
Multiple suspended transaction ids in the same active context are not supported by this model.
| this.expenseReportRepository = expenseReportRepository | ||
| } | ||
|
|
||
| @Transactional(propagation = TransactionDefinition.Propagation.SUSPEND, timeout = 3600) |
There was a problem hiding this comment.
Maybe suspend-resume would be implicit if Oracle session state is present?
There was a problem hiding this comment.
SUSPEND and REQUIRES_SUSPENDED have just been moved out of the core propagation enum and into @OracleTransactional so the Oracle-specific behavior is explicit. I don’t think suspend/resume should be implicit from the presence of Oracle sessionless state, because @OracleTransactional can also be used for other Oracle transaction options, such as transaction priority, where the transaction should still behave like a normal transaction.
The propagated sessionless state should only carry the GTRID. The annotation should decide whether the method starts/suspends or resumes/completes a sessionless transaction.
| * @since 5.1.0 | ||
| */ | ||
| @Experimental | ||
| public interface OracleSessionlessTransactionIdCodec { |
There was a problem hiding this comment.
Do we need this as a interface? Can we have multiple implementations?
There was a problem hiding this comment.
The reason for the interface is to let applications replace the default wire format/security policy for the GTRID. The GTRID becomes a capability token when it is propagated over HTTP or another transport, so some applications may want to:
- sign the encoded value to detect tampering
- encrypt it so the raw Oracle GTRID is not exposed
- bind it to a tenant/user/workflow
- add expiry metadata
The default implementation is simple: URL-safe Base64 without padding. A custom implementation would replace it as the single codec used by both HTTP propagation and programmatic propagation.
So no, multiple codecs are not intended to be active at the same time. Micronaut bean resolution should select one.
propagation values with OracleTransactional.Sessionless modes
|



Adds Oracle JDBC sessionless transaction support to Micronaut Data. The implementation introduces SUSPEND and REQUIRES_SUSPENDED transaction propagation modes. SUSPEND starts an Oracle sessionless transaction, stores the returned GTRID in propagated state, and suspends instead of committing. REQUIRES_SUSPENDED reads the propagated GTRID, resumes the Oracle transaction, then commits or rolls back normally and clears the state.
The feature includes opt-in HTTP propagation through a server filter. The filter owns request-scoped sessionless transaction state, imports the GTRID from the configured header, and exports a suspended GTRID on the response. The default header is Oracle-Sessionless-Transaction-Id.
Non-HTTP propagation is supported through OracleSessionlessTransactionPropagationOperations, allowing callers to create lexical propagation scopes, export the current encoded transaction id, and later import it for resume workflows.
GTRID string conversion is handled by OracleSessionlessTransactionIdCodec. The default codec uses URL-safe Base64 without padding, and applications can replace it to add signing, encryption, expiration metadata, or transport-specific encoding.
Unsupported transaction managers now fail fast for SUSPEND / REQUIRES_SUSPENDED instead of silently treating them as normal transactions.